package club.gggd.security_jwt_demo.util;

import club.gggd.security_jwt_demo.pojo.entity.SysUser;
import club.gggd.security_jwt_demo.pojo.module.*;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

/**
 * @Description jwt工具类
 * @Author srx
 * @date 2023/12/10 13:27
 */
@Data
@Component
@ConfigurationProperties("jwt")
public class JwtUtil {

    private String requestHeader;
    private String secret;
    private int expiration;
    private String tokenStartWith;

    @Value("${login.is-share}")
    private Boolean isShare;

    @Value("${login.login-model}")
    private String loginModel;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 获取用户名
     * @return
     */
    public String getAccountByToken(String token) {
        Claims claims = getClaimsByToken(token);
        return claims != null ? claims.getSubject() : null;
    }

    /**
     * 获取过期时间
     * @param token
     * @return Date
     */
    public Date getExpiredByToken(String token) {
        Claims claims = getClaimsByToken(token);
        return claims != null ? claims.getExpiration() : null;
    }

    /**
     * 获取Claims
     * @param token
     * @return
     */
    private Claims getClaimsByToken(String token) {
        Claims claims;
        try {
            claims = Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            // 签名不一致异常
            if (e instanceof SignatureException) {
                throw new TokenException(ResultCode.TOKEN_INVALID);
            }
            // token过期异常
            if (e instanceof ExpiredJwtException) {
                throw new TokenException(ResultCode.TOKEN_TIMEOUT);
            }
            // 如果都不是上面的则弹出token无效异常
            throw new TokenException(ResultCode.TOKEN_INVALID);
        }
        return claims;
    }

    /**
     * 计算过期时间
     * @return
     */
    private Date generateExpired() {
        return new Date(System.currentTimeMillis() + expiration * 1000);
    }

    /**
     * 判断 Token 是否过期
     * @param token
     * @return
     */
    private Boolean isTokenExpired(String token) {
        Date expirationDate = getExpiredByToken(token);
        return expirationDate.before(new Date());
    }

    /**
     * 获取用户session信息
     * @param account
     * @return
     */
    private TokenInfo getSessionInfo(String account) {
        String key = TokenConstant.SESSION + account;
        TokenInfo tokenInfo = (TokenInfo) redisUtil.get(key);
        if (tokenInfo == null) {
            tokenInfo = new TokenInfo();
            tokenInfo.setId(key);
            tokenInfo.setCreateTime(System.currentTimeMillis());
            List<TokenSign> tokenSignList = new ArrayList<>();
            tokenInfo.setTokenSignList(tokenSignList);
        }
        // 删除过期token
        long cur = System.currentTimeMillis();
        tokenInfo.getTokenSignList().removeIf(e -> e.getDeadline() < cur);
        return tokenInfo;
    }

    /**
     * 设置token和session到redis
     * @param token
     * @param device
     * @param account
     * @param sessionInfo
     */
    private void setTokenAndSessionHandle(String token, String device, String account, TokenInfo sessionInfo) {
        // 将token存入
        redisUtil.set(TokenConstant.TOKEN + token, account);
        // 组装新的token
        TokenSign tokenSign = new TokenSign();
        tokenSign.setToken(token);
        tokenSign.setDevice(device);
        tokenSign.setDeadline(System.currentTimeMillis() + expiration * 1000);
        sessionInfo.getTokenSignList().add(tokenSign);
        // 把session存入
        redisUtil.set(TokenConstant.SESSION + account, sessionInfo);
    }

    /**
     * 单设备登录
     * @param token
     * @param account
     * @param device
     */
    private void aloneLogin(String token, String account, String device) {
        // 拿到当前用户的session信息
        TokenInfo sessionInfo = getSessionInfo(account);
        List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
        // token不为空时清空所有token
        if (CollectionUtil.isNotEmpty(tokenSignList)) {
            for (TokenSign t : tokenSignList) {
                redisUtil.del(TokenConstant.TOKEN + t.getToken());
            }
            tokenSignList.clear();
        }
        setTokenAndSessionHandle(token, device, account, sessionInfo);
    }

    /**
     * 多设备登录
     * @param token
     * @param account
     * @param device
     * @return token
     */
    private String multiLogin(String token, String account, String device) {
        // 拿到当前用户的session信息
        TokenInfo sessionInfo = getSessionInfo(account);
        List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
        // 如果是共享token且session里面存在token
        if (isShare && CollectionUtil.isNotEmpty(tokenSignList)) {
            // 则直接返回原来的token
            return tokenSignList.get(0).getToken();
        }
        // 没有开启共享token，或开启了共享token但是是第一次登录
        setTokenAndSessionHandle(token, device, account, sessionInfo);
        return token;
    }

    /**
     * 同端互斥登录
     * @param token
     * @param account
     * @param device
     */
    private void sameDeviceExclusionLogin(String token, String account, String device) {
        TokenInfo sessionInfo = getSessionInfo(account);
        List<TokenSign> tokenSignList = sessionInfo.getTokenSignList();
        // 判断是否存在当前设备的登录信息
        Iterator<TokenSign> iterator = tokenSignList.iterator();
        while (iterator.hasNext()) {
            TokenSign next = iterator.next();
            if (next.getDevice().equals(device)) {
                // 存在则删除
                redisUtil.del(TokenConstant.TOKEN + next.getToken());
                iterator.remove();
                break;
            }
        }
        // 录入当前登录信息
        setTokenAndSessionHandle(token, device, account, sessionInfo);
    }

    /**
     * 生成 Token
     * @param user 用户信息
     * @return
     */
    public String generateToken(SysUser user) {
        String token = Jwts.builder()
                .setSubject(user.getAccount())
                .setExpiration(generateExpired())
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        String device = UserAgentUtil.getDevice();
        // 判断登录模式
        if (LoginModel.ALONE.getValue().equals(loginModel)) {
            // 单设备登录
            aloneLogin(token, user.getAccount(), device);
        } else if (LoginModel.MULTI.getValue().equals(loginModel)) {
            // 多设备登录
            String newToken = multiLogin(token, user.getAccount(), device);
            return newToken;
        } else {
            // 同端互斥登录
            sameDeviceExclusionLogin(token, user.getAccount(), device);
        }

        return token;
    }

    /**
     * 验证 Token
     * @param token
     * @return
     */
    public Boolean validateToken(String token) {
        // 调用这个方法主要是用来验证签名和有效期
        getClaimsByToken(token);
        String key = TokenConstant.TOKEN + token;
        return StrUtil.isNotEmpty(token) && !isTokenExpired(token) && redisUtil.hasKey(key);
    }

    /**
     * 移除 Token
     * @param token
     */
    public void removeToken(String token) {
        // 获取session信息
        String account = getAccountByToken(token);
        String key = TokenConstant.TOKEN + token;
        TokenInfo tokenInfo = (TokenInfo) redisUtil.get(TokenConstant.SESSION + account);
        // 删除该token
        tokenInfo.getTokenSignList().removeIf(e -> e.getToken().equals(token));
        redisUtil.set(TokenConstant.SESSION + account, tokenInfo);
        redisUtil.del(key);
        // 如果此时该账号没有token了，则删除UserDetails
        if (CollectionUtils.isEmpty(tokenInfo.getTokenSignList())) {
            delUserDetail(account);
        }
    }

    /**
     * 获取token
     * @param request
     * @return
     */
    public String getToken(HttpServletRequest request) {
        String auth = request.getHeader(requestHeader);
        if (StrUtil.isBlank(auth)) {
            return null;
        }
        String token = auth.replace(tokenStartWith, "");
        return token;
    }

    /**
     * 退出登录
     */
    public void logout(HttpServletRequest request) {
        String token = getToken(request);
        removeToken(token);
    }

    /**
     * 获取userDetail
     * @param token
     * @return
     */
    public JwtUser getUserDetail(String token) {
        String account = getAccountByToken(token);
        String s = (String) redisUtil.get("user:userDetail:" + account);
        JwtUser jwtUser = JSON.parseObject(s, JwtUser.class);
        return jwtUser;
    }

    /**
     * 删除userDetail
     * @param account
     */
    public void delUserDetail(String account) {
        redisUtil.del("user:userDetail:" + account);
    }
}
